﻿using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Text;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace CodeAnalysisRuleCollection.Common.CSharp
{
    /// <example>public string Name { get { return _name; } set { _name = value; } }</example>
    [ExportCodeRefactoringProvider("ConvertToAutoProperty", LanguageNames.CSharp)]
    internal partial class ConvertToAutoPropertyCodeRefactoringProvider : ICodeRefactoringProvider
    {
        public async Task<IEnumerable<CodeAction>> GetRefactoringsAsync(Document document, TextSpan span, CancellationToken cancellationToken)
        {
            var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

            // find property declaration
            var property = root.FindToken(span.Start).Parent.FirstAncestorOrSelf<PropertyDeclarationSyntax>();
            if (property == null || property.AccessorList == null)
                return null;
            
            // check if collapsible and get field name
            string fieldName = null;

            if (IsCollapsible(property, out fieldName, cancellationToken))
                return new[] { CodeAction.Create("Collapse to auto-property", c => ConvertToAutoPropertyAsync(root, document, property, fieldName, c)) };

            return null;
        }

        internal static bool IsCollapsible(PropertyDeclarationSyntax property, out string fieldName, CancellationToken c)
        {
            fieldName = null;

            // getter
            AccessorDeclarationSyntax getter = property.AccessorList.Accessors.SingleOrDefault(a => a.IsKind(SyntaxKind.GetAccessorDeclaration));
            if (getter == null || getter.Body == null)
                return false;

            var returnExpr = getter.Body.GetEffectiveStatement() as ReturnStatementSyntax;
            if (returnExpr == null ||
                returnExpr.Expression == null ||
                !returnExpr.Expression.IsKind(SyntaxKind.IdentifierName)
            )
                return false;

            // setter
            AccessorDeclarationSyntax setter = property.AccessorList.Accessors.SingleOrDefault(a => a.IsKind(SyntaxKind.SetAccessorDeclaration));
            if (setter == null || setter.Body == null)
                return false;

            var statementExpr = setter.Body.GetEffectiveStatement() as ExpressionStatementSyntax;
            if (statementExpr == null)
                return false;

            var setExpr = statementExpr.Expression as BinaryExpressionSyntax;
            if (setExpr == null ||
                !setExpr.IsKind(SyntaxKind.SimpleAssignmentExpression) ||
                setExpr.Left == null || setExpr.Right == null ||
                !setExpr.Right.IsKind(SyntaxKind.IdentifierName)
            )
                return false;

            // match experssions
            if (!SyntaxFactory.AreEquivalent(returnExpr.Expression, setExpr.Left) ||
                !setExpr.Right.IsKind(SyntaxKind.IdentifierName) ||
                (setExpr.Right as IdentifierNameSyntax).Identifier.ValueText != "value"
            )
                return false;

            fieldName = (returnExpr.Expression as IdentifierNameSyntax).Identifier.ValueText; // TODO: Handle if it is a member access expression
            return true;
        }

        protected virtual async Task<Document> ConvertToAutoPropertyAsync(SyntaxNode root, Document document, PropertyDeclarationSyntax property, string fieldName, CancellationToken c)
        {
            var model = await document.GetSemanticModelAsync(c).ConfigureAwait(false);

            // find declaring type
            TypeDeclarationSyntax type = property.FirstAncestorOrSelf<TypeDeclarationSyntax>();

            // remove backing field
            EqualsValueClauseSyntax initializer = null;
            TypeDeclarationSyntax newType = type
                .WithoutBackingField(fieldName, document.Project.Solution.Workspace, out initializer);

            // find property declaration in new type
            property = newType.Members.OfType<PropertyDeclarationSyntax>().Single(m => property.IsEquivalentTo(m));

            AccessorDeclarationSyntax
                getter = property.AccessorList.Accessors.Single(a => a.IsKind(SyntaxKind.GetAccessorDeclaration)),
                setter = property.AccessorList.Accessors.Single(a => a.IsKind(SyntaxKind.SetAccessorDeclaration)); 

            // expand accessors
            var newNode = property
                .WithAccessorList(
                    SyntaxFactory.AccessorList(
                        new SyntaxList<AccessorDeclarationSyntax>()
                            .Add(
                                getter.WithBody(null)
                                    .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))
                            )
                            .Add(
                                setter.WithBody(null)
                                    .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))
                            )
                    )
                )
                .WithAdditionalAnnotations(Formatter.Annotation)
            ;

            if (initializer != null)
                newNode = newNode
                    .WithInitializer(initializer)
                    .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))
                ;

            // replace nodes
            newType = newType.ReplaceNode(property, newNode);

            var newRoot = root
                .ReplaceNode(type, newType)
            ;

            return document.WithSyntaxRoot(newRoot);
        }
    }
}
